﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Mono.Cecil;
using System.Reflection;
using Mono.Cecil.Cil;
using System.Web.UI;

namespace Bewise.Sharepoint.WebPartator
{
    public class WebPartInjector
    {
        private AssemblyDefinition m_asm;

        public WebPartInjector(AssemblyDefinition asm)
        {
            m_asm = asm;
        }

        public TypeDefinition CreateType(TypeDefinition userControl)
        {
            TypeReference typeRef = m_asm.MainModule.Import(typeof(System.Web.UI.WebControls.WebParts.WebPart));
            TypeDefinition typeDef = new TypeDefinition(userControl.Name + "WebPart", userControl.Namespace, Mono.Cecil.TypeAttributes.Public, typeRef);
            typeDef.IsClass = true;
            typeDef.IsBeforeFieldInit = true;

            typeDef.Fields.Add(CreateField(userControl));
            typeDef.Constructors.Add(CreateConstructor(userControl, typeDef.Fields[0]));
            CloneProperties(typeDef, userControl, typeDef.Fields[0]);
            typeDef.Methods.Add(CreateRenderControlMethod(typeDef,typeDef.Fields[0]));

            return typeDef;
        }

        protected FieldDefinition CreateField(TypeDefinition userControl)
        {
            return new FieldDefinition("m_field", userControl, Mono.Cecil.FieldAttributes.Private);
        }

        protected MethodDefinition CreateConstructor(TypeDefinition typeDef, FieldDefinition field)
        {
            Mono.Cecil.MethodAttributes ctorAttributes = Mono.Cecil.MethodAttributes.RTSpecialName | Mono.Cecil.MethodAttributes.SpecialName | Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.HideBySig;
            MethodDefinition ctorDef = new MethodDefinition(".ctor", ctorAttributes, m_asm.MainModule.Import(typeof(void)));
            BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
            ConstructorInfo webPartCtor = typeof(System.Web.UI.WebControls.WebParts.WebPart).GetConstructor(flags, null, Type.EmptyTypes, null);
            CilWorker ctorWorker = ctorDef.Body.CilWorker;
            ctorWorker.Emit(OpCodes.Ldarg_0);
            ctorWorker.Emit(OpCodes.Call, m_asm.MainModule.Import(webPartCtor));
            ctorWorker.Emit(OpCodes.Ldarg_0);
            ctorWorker.Emit(OpCodes.Newobj, typeDef.Constructors[0]);
            ctorWorker.Emit(OpCodes.Stfld, field);
            ctorWorker.Emit(OpCodes.Ret);
            return ctorDef;
        }

        protected void CloneProperties(TypeDefinition typeDef, TypeDefinition userControl, FieldDefinition usrField)
        {
            TypeDefinition baseTypeDef = (TypeDefinition)userControl.BaseType;
            foreach (PropertyDefinition propDef in baseTypeDef.Properties)
            {
                typeDef.Properties.Add(CloneProperty(userControl, propDef, usrField));
            }
        }

        protected PropertyDefinition CloneProperty(TypeDefinition userControl, PropertyDefinition propertyDef, FieldDefinition usrField)
        {
            PropertyDefinition prop = new PropertyDefinition(propertyDef.Name, propertyDef.PropertyType, (Mono.Cecil.PropertyAttributes)0);
            MethodDefinition getterDef = new MethodDefinition("get_" + propertyDef.Name, Mono.Cecil.MethodAttributes.Public, propertyDef.GetMethod.ReturnType.ReturnType);
            MethodDefinition setterDef = new MethodDefinition("set_" + propertyDef.Name, Mono.Cecil.MethodAttributes.Public, propertyDef.SetMethod.ReturnType.ReturnType);

            VariableDefinition varDef = new VariableDefinition(getterDef.ReturnType.ReturnType);

            getterDef.Body.Variables.Add(varDef);
            getterDef.Body.InitLocals = true;

            ParameterDefinition setterParamDef = new ParameterDefinition(propertyDef.PropertyType);
            setterDef.Parameters.Add(setterParamDef);
            foreach (CustomAttribute ca in propertyDef.CustomAttributes)
                prop.CustomAttributes.Add(ca);

            userControl.Methods.Add(getterDef);
            userControl.Methods.Add(setterDef);
            prop.GetMethod = getterDef;
            prop.SetMethod = setterDef;

            CilWorker setterWorker = prop.SetMethod.Body.CilWorker;
            prop.SetMethod.Body.Instructions.Clear();
            setterWorker = GenerateSetter(setterWorker, propertyDef, usrField);

            prop.GetMethod.Body.Instructions.Clear();
            CilWorker getterWorker = prop.GetMethod.Body.CilWorker;
            getterWorker = GenerateGetter(getterWorker, propertyDef, usrField);

            return prop;
        }

        private CilWorker GenerateSetter(CilWorker worker, PropertyDefinition propDef, FieldDefinition field)
        {
            worker.Emit(OpCodes.Ldarg_0);
            worker.Emit(OpCodes.Ldfld, field);
            worker.Emit(OpCodes.Ldarg_1);
            worker.Emit(OpCodes.Callvirt, propDef.SetMethod);
            worker.Emit(OpCodes.Ret);
            return worker;
        }

        private CilWorker GenerateGetter(CilWorker worker, PropertyDefinition propDef, FieldDefinition field)
        {
            worker.Emit(OpCodes.Ldarg_0);
            worker.Emit(OpCodes.Ldfld, field);
            worker.Emit(OpCodes.Callvirt, propDef.GetMethod);
            worker.Emit(OpCodes.Stloc_0);
            worker.Emit(OpCodes.Ldloc_0);
            worker.Emit(OpCodes.Ret);
            return worker;
        }

        private MethodDefinition CreateRenderControlMethod(TypeDefinition typeDef, FieldDefinition field)
        {
            Mono.Cecil.MethodAttributes attributes = Mono.Cecil.MethodAttributes.Virtual | Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.HideBySig;

            MethodDefinition methodDef = new MethodDefinition("RenderControl", attributes, m_asm.MainModule.Import(typeof(void)));
            ParameterDefinition renderParam = new ParameterDefinition(m_asm.MainModule.Import(typeof(HtmlTextWriter)));
            renderParam.Name = "writer";
            methodDef.Parameters.Add(renderParam);

            CilWorker worker = methodDef.Body.CilWorker;
            List<Instruction> instructions = new List<Instruction>();
            worker.Emit(OpCodes.Nop);
            worker.Emit(OpCodes.Ldarg_0);
            MethodInfo getControlsMethod =
                typeof(System.Web.UI.Control).GetMethod("get_Controls");

            MethodInfo addControlsMethod =
                typeof(System.Web.UI.ControlCollection).GetMethod("Add");

            MethodReference getControlsRef = m_asm.MainModule.Import(getControlsMethod);
            worker.Emit(OpCodes.Callvirt, getControlsRef);
            worker.Emit(OpCodes.Ldarg_0);
            worker.Emit(OpCodes.Ldfld, field);
            MethodReference addControlsRef = m_asm.MainModule.Import(addControlsMethod);
            worker.Emit(OpCodes.Callvirt, addControlsRef);
            worker.Emit(OpCodes.Ldarg_0);
            worker.Emit(OpCodes.Ldarg_1);
            MethodInfo renderControlMethod = typeof(System.Web.UI.Control).GetMethod("RenderControl", new Type[] { typeof(HtmlTextWriter) });
            MethodReference renderControlRef = m_asm.MainModule.Import(renderControlMethod);
            worker.Emit(OpCodes.Call, renderControlRef);
            worker.Emit(OpCodes.Ret);
            typeDef.Methods.Add(methodDef);
            return methodDef;
        }
    }
}
